package org.nativejpa.transaction;

import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceException;
import javax.persistence.TransactionRequiredException;
import javax.transaction.Status;
import javax.transaction.TransactionManager;
import javax.transaction.Transaction;
import javax.transaction.Synchronization;
import javax.transaction.NotSupportedException;
import javax.transaction.InvalidTransactionException;
import javax.transaction.SystemException;
import javax.transaction.RollbackException;

/**
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 * @create 2009-3-17 15:21:56
 */
public class EntityTransactionImpl implements EntityTransaction {

    private TransactionManager tm = null;

    private Transaction currentTx = null;

    private boolean joined = false;

    private Transaction suspendedTx = null;

    public EntityTransactionImpl(TransactionManagerFactory transactionManagerFactory) {
        this.tm = transactionManagerFactory.getTransactionManager();
    }

    public Transaction getInternalTransaction(){
        try {
        if(tm.getStatus() != Status.STATUS_NO_TRANSACTION) {
            return tm.getTransaction();
        }
        else {
            return null;
        }
        }
        catch (SystemException e ) {
            throw new TransactionException(e);
        }
    }

    // EntityTrandaction implementation, can invoke only non ContainerManaged
    public void begin() {
        int status = Status.STATUS_UNKNOWN;
        try {
            status = tm.getStatus();
        }
        catch (SystemException e) {
            throw new TransactionException("Failed to get transaction status.", e);
        }
        if (status == Status.STATUS_NO_TRANSACTION) {
            // no active status bound with current thread
            try {
                tm.begin();
                currentTx = tm.getTransaction();
                currentTx.registerSynchronization(new Synchronization() {
                    public void afterCompletion(int i) {
                        // set current activeTx = null
                        currentTx = null;
                    }

                    public void beforeCompletion() {
                        // do nothing
                    }
                });
            }
            catch (Exception e) {
                throw new PersistenceException("EntityTransaction.begin failed!", e);
            }
        }
        else { // an active transaction bound with current thread
            try {
                Transaction tx = tm.getTransaction();
                if (tx == currentTx) { // this EntityManager's transaction, throw exception
                    throw new TransactionException("failed to begin a new transaction.", new NotSupportedException("A transaction is already associated with current EntityManager"));
                }
                else { // other EntityManager's transaction, suspend, and begin new, resume previous after commit
                    final Transaction suspendedTx = tm.suspend();
                    try {
                        tm.begin();
                    }
                    catch (NotSupportedException e) {
                        throw new TransactionException(e);
                    }
                    currentTx = tm.getTransaction();
                    try {
                        currentTx.registerSynchronization(new Synchronization() {
                            public void afterCompletion(int i) {
                                //!!! Transaction should be commit serially, only one active trsanction in one time, Last create, First commit
                                // set current activeTx = null
                                currentTx = null;
                                // resume supendedTx
                                try {
                                    tm.resume(suspendedTx);
                                }
                                catch (InvalidTransactionException e) {
                                    throw new TransactionException(e);
                                }
                                catch (SystemException e) {
                                    throw new TransactionException(e);
                                }
                            }

                            public void beforeCompletion() {
                            }
                        });
                    }
                    catch (RollbackException e) {
                        throw new TransactionException(e);
                    }
                }
            }
            catch (SystemException e) {
                throw new TransactionException(e);
            }
        }
    }

    public void commit() {
        try {
            tm.commit();
        }
        catch (Exception e) {
            throw new TransactionException("EntityTransaction.commit failed!", e);
        }
    }

    public boolean getRollbackOnly() {
        try {
            return tm.getStatus() == Status.STATUS_MARKED_ROLLBACK;
        }
        catch (Exception e) {
            throw new TransactionException("EntityTransaction.isActive failed!", e);
        }
    }

    public boolean isActive() {
        try {
            return tm.getStatus() == Status.STATUS_ACTIVE;
        }
        catch (Exception e) {
            throw new TransactionException("EntityTransaction.isActive failed!", e);
        }
    }

    public void rollback() {
        try {
            tm.rollback();
        }
        catch (Exception e) {
            throw new TransactionException("EntityTransaction.rollback failed!", e);
        }
    }

    public void setRollbackOnly() {
        try {
            tm.setRollbackOnly();
        }
        catch (Exception e) {
            throw new TransactionException("EntityTransaction.setRollbackOnly failed!", e);
        }
    }

    public void joinTransaction() {
        try {
            if (tm.getStatus() != Status.STATUS_ACTIVE) { // can not join, no active status bound with current thread
                throw new TransactionRequiredException("Transaction status: " + tm.getStatus());
            }
        }
        catch (SystemException e) {
            throw new TransactionException("Failed to getStatus.", e);
        }
        try {
            currentTx = tm.getTransaction();
            joined = true;
            currentTx.registerSynchronization(new Synchronization() {
                public void afterCompletion(int i) {
                    joined = false;
                    currentTx = null;
                }

                public void beforeCompletion() {

                }
            });
        }
        catch (SystemException e) {
            throw new TransactionException(e);
        }
        catch (RollbackException e) {
            throw new TransactionException(e);
        }
    }

    public boolean hasJoinedTransaction() {
        return joined;
    }

    // invoke in TransactionQuerySynchronization.beforeQuery
    // try suspend other EntityManager's Transaction if current Entity does NOT begin transaction and the First EntityManager started one
    // TransactionQuerySynchronization is only registered in NON ContainerManaged!!!
    public void suspendTransaction() {
        if (hasJoinedTransaction()) { // has joined Transaction, skip split
            return;
        }
        try {
            if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { // transaction in current thread
                if (currentTx == null) { // no transaction in current EntityManager
                    //TransactionQuerySynchronization 来恢复 suspendedTx
                    suspendedTx = tm.suspend();
                }
            }
        }
        catch (SystemException e) {
            throw new TransactionException("Failed to suspendTransaction.", e);
        }
    }

    // invoked in TransactionQuerySynchronization.afterQuery, try to resume Tx that has been suspended
    public void resumeTransaction() {
        try {
            if (suspendedTx != null) {
                tm.resume(suspendedTx);
            }
        }
        catch (SystemException e) {
            throw new TransactionException("Failed to resumeTransaction.", e);
        }
        catch (InvalidTransactionException e) {
            throw new TransactionException("Failed to resumeTransaction.", e);
        }
        finally {
            suspendedTx = null;
        }
    }

}
